package lumis.doui;

import java.text.MessageFormat;

import javax.servlet.jsp.el.ELException;
import javax.servlet.jsp.el.VariableResolver;

import lumis.portal.PortalException;
import lumis.portal.UnexpectedException;
import lumis.portal.el.LumisExpressionEvaluatorImpl;
import lumis.util.XmlUtil;
import lumis.util.log.ILogger;
import lumis.util.log.LoggerFactory;

import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * 
 *	Responsible for processing the logic tag in the douidefinition file.  For the process to conclude successfully, 
 *  the logic tags must obey the following syntax:
 *
 * 	<pre>
 * 	&lt;doui:douiDefinition xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 * 						xmlns:doui="http://www.lumis.com.br/lumisportal/xsd/doui"	
 * 						xmlns:control="http://www.lumis.com.br/douicontrols" 
 * 						xmlns:logic="http://www.lumis.com.br/lumisportal/xsd/doui/logic"&gt;
 * 
 * 		&lt;service id="myId"&gt;
 * 			...
 * 		&lt;/service&gt;
 * 		&lt;interfaces&gt;
 * 			...
 * 			&lt;logic:choose&gt;
 * 				&lt;logic:when test="empty param.lumItemId"&gt;
 * 					...
 * 				&lt;/logic:when&gt;
 *				&lt;logic:when test="param.instanceId eq 'add'"&gt;
 *					...
 * 				&lt;/logic:when&gt;
 * 				&lt;logic:otherwise&gt;
 * 					...
 * 				&lt;/logic:otherwise&gt;
 * 			&lt;/logic:choose&gt;
 * 			...
 * 		&lt;/interfaces&gt;
 * 	&lt;/doui:doui:douiDefinition&gt;
 *	</pre>
 *	The process will happen top down, and in finding the first <code>when</code> that has its test true, all other tags
 *	for the current <code>choose</code> will be ignored.
 *
 * @version $Revision: 11804 $ $Date: 2010-07-29 10:06:21 -0300 (Thu, 29 Jul 2010) $
 * @since 5.6.0
 */
public class DouiLogicTagProcessor
{
	private static final String LOGIC_NAMESPACE_URI = "http://www.lumis.com.br/lumisportal/xsd/doui/logic";
	private static final String DOUI_TAG_WHEN_TEST_ATTRIBUTE = "test";
	private static final String DOUI_TAG_OTHERWISE = "otherwise";
	private static final String DOUI_TAG_WHEN = "when";
	private static final MessageFormat DOUI_TAG_QUERY = new MessageFormat("{0}:{1}");
	private static final String DOUI_TAG_CHOOSE = "choose";
	private static final String INTERFACE_TAG = "<interface";
	private static final MessageFormat INTERFACE_TAG_NAMESPACE_REPLACEMENT = new MessageFormat(INTERFACE_TAG + " xmlns:{0}=\""+ LOGIC_NAMESPACE_URI +"\"");
	
	/**
	 * Identifies the xml node attribute that will determine if the service interface contains a logic tag.
	 * @since 5.6.0
	 */
	public static final String CONTAINS_TAG_NODE_ATTRIBUTE = "containsLogicTag";
	
	private static ILogger logger = LoggerFactory.getLogger(DouiLogicTagProcessor.class);
	
	private final Node serviceInterfaceDefinitionNode;
	private boolean tagOcurrences = false;
	private String tagPrefix;
	
	/**
	 * Only constructor that receives the service interface definition as a node.
	 * 
	 * @param serviceInterfaceDefinitionNode service definition node that needs to be processed.
	 * @throws PortalException If an exception occurs during xml processing.
	 * @since 5.6.0
	 */
	public DouiLogicTagProcessor(Node serviceInterfaceDefinitionNode) throws PortalException
	{
		this.serviceInterfaceDefinitionNode = serviceInterfaceDefinitionNode;
		setPrefix();
		verifyTagOcurrences();
	}

	/**
	 * Retrieves if the node contains any ocurrences of the doui logic tags.
	 * @return true if the service node interface contains any doui logic tags, false otherwise. 
	 * @since 5.6.0
	 */
	public boolean hasTagOcurrences()
	{
		return this.tagOcurrences;
	}
	
	/**
	 * Retrives the prefix of the doui logic tags present in the service interface definition.
	 * @return <code>null</code> if the service interface definition if the douidefinition file did not contain the logic 
	 * 	namespace, otherwise, returns the prefix for that namespace.
	 * @since 5.6.0
	 */
	public String getTagPrefix()
	{
		return this.tagPrefix;
	}
	
	/**
	 * Processes the doui logic tags present in the service definition node.  
	 * Once if finds the first <code>when</code>, in the <code>choose</code> tag, that has its test true, 
	 * it will ignore the remaining <code>when</code> and the <code>otherwise</code> tags.  
	 * @param evaluator the Expression Language evaluator to use when processing the doui logic tags.
	 * @param variableResolver the Variable Resolver that will acuratly resolve the variables when processing the doui logic tags.
	 * @throws PortalException If an exception occurs during xml processing or el evaluation.
	 * @since 5.6.0
	 */
	
	public void doProcessTags(LumisExpressionEvaluatorImpl evaluator, VariableResolver variableResolver) throws PortalException
	{
		if(hasTagOcurrences())
			processNode(serviceInterfaceDefinitionNode, evaluator, variableResolver);
	}
	
	/**
	 * Responsible for recursively search the elements and execute the conditions.
	 * @param root node that will be processed
	 * @param evaluator the Expression Language evaluator to use when processing the doui logic tags.
	 * @param variableResolver the Variable Resolver that will acuratly resolve the variables when processing the doui logic tags.
	 * @throws PortalException If an exception occurs during xml processing or el evaluation.
	 * @since 5.6.0
	 */
	private void processNode(Node root, LumisExpressionEvaluatorImpl evaluator, VariableResolver variableResolver) throws PortalException 
	{
		
		//if current node has no children, exit
		if(!root.hasChildNodes())
		{
			return ;
		}
		
		NodeList childNodes = root.getChildNodes();
		//for each child node that is an ElementNode
		for (int i = 0; i < childNodes.getLength(); i++)
		{
			Node element = childNodes.item(i);
			if(element.getNodeType() != Node.ELEMENT_NODE)
				continue;
			
			//if the current child node is a "choose" tag, 
			//promote its children, remove it from tree and mark that 
			if(containsPrefix(element) &&  element.getLocalName().equals(DOUI_TAG_CHOOSE))
			{	
				promoteChildren(element);
				root.removeChild(element);
				i--;
				continue;

			}
			else
			{
				//for the current child node be a "when" tag
				if(containsPrefix(element) && element.getLocalName().equals(DOUI_TAG_WHEN))
				{					
					String el = XmlUtil.readAttributeString(DouiLogicTagProcessor.DOUI_TAG_WHEN_TEST_ATTRIBUTE, element);
					try
					{
						
						boolean elValue = ((Boolean)evaluator.evaluate("${"+ el +"}", Boolean.class, variableResolver, null)).booleanValue();
						
						if (logger.isDebugEnabled())
							logger.debug("The expression '" + el + "' was evaluated as " + elValue);
						
						//if the condition is met
						if (elValue)
						{
							//remove any other condition tag for the currently processing conditions tag  
							Node[] conditionNodes = XmlUtil.selectNodes(DOUI_TAG_QUERY.format(new Object[]{getTagPrefix(), DOUI_TAG_WHEN}), root);
							for (Node n: conditionNodes)
								if (n != element)
									root.removeChild(n);
							
							//remove the default (otherwise) tag for the currently processing conditions tag
							Node defaultNode = XmlUtil.selectSingleNode(DOUI_TAG_QUERY.format(new Object[]{getTagPrefix(), DOUI_TAG_OTHERWISE}), root);
							if (defaultNode != null)
								root.removeChild(defaultNode);
							
							//promote children in order to continue processing
							promoteChildren(element);
						}
					}
					catch (DOMException e)
					{
						throw new UnexpectedException(e);
					}
					catch (ELException e)
					{
						throw new UnexpectedException(e);
					}
					
					//remove current condition(when):
					//	- if the condition was not met it needs to be discarted
					//	- if the condition was met, its siblings have already been removed
					//		and so should it be
					root.removeChild(element);
					i--;
					continue;
				}
				else if(containsPrefix(element) && element.getLocalName().equals(DOUI_TAG_OTHERWISE))
				{
					//no when tag was succesfull, thus needing to use the default value

					//promote children in order to continue processing
					//and remove it from elements to process, since it's process is finished
					promoteChildren(element);
					root.removeChild(element);

					i--;
					continue;
				}
				else
				{
					//this should only happen when all "choose" tags processing has been done
					//in which case, should process the current sibling's children
					processNode(element, evaluator, variableResolver);
				}
			}
		}		
		return;
	}

	/**
	 *  Verifies if the current node contains the prefix for the logic namespace.	 
	 * @param element the node which will be verified if the prefix exists
	 * @return true if the node's prefix is the same as the doui logic tags'.
	 * @since 5.6.0
	 */
	private boolean containsPrefix(Node element)
	{
		//prefix usually would not be null, since it will hardly be set as the default namespace
		return this.tagPrefix != null && this.tagPrefix.equals(element.getPrefix());
	}

	/**
	 * Promotes the children to the same hierarchy place as the current node.
	 * @param node node who's children need to be promoted.
	 * @since 5.6.0
	 */
	private void promoteChildren(Node node)
	{
		NodeList childNodes = node.getChildNodes();
		//for each element child node
		for (int j = 0; j < childNodes.getLength(); j++)
		{
			Node child = childNodes.item(j);
			if(child.getNodeType() == Node.ELEMENT_NODE)
			{
				//remove from current parent and insert its parent's simbling
				Node removed = node.removeChild(child);
				node.getParentNode().insertBefore(removed, node);
			}
		}
	}
	
	/**
	 * Verifies if the current service instance definition contains any logic tags.
	 * @throws PortalException If an exception occurs during xml processing. 
	 * @since 5.6.0
	 */
	private void verifyTagOcurrences() throws PortalException
	{
		String documentString = XmlUtil.getXmlString(serviceInterfaceDefinitionNode);
		//adds namespace from document to current servide interface definition node
		documentString = documentString.replaceFirst(INTERFACE_TAG, INTERFACE_TAG_NAMESPACE_REPLACEMENT.format(new Object[]{getTagPrefix()}));
		//converts node to document
		Document tempDoc = XmlUtil.getDocument(documentString);
		//verifies the existance of any nodes containing the tag prefix for that namespace 
		NodeList nodeList = tempDoc.getElementsByTagNameNS(LOGIC_NAMESPACE_URI, DOUI_TAG_CHOOSE);
		
		//sets it to the defined attribute, in order not to execute this transformation everytime it needs to verify if service interface contains
		//any tags
		this.tagOcurrences = nodeList.getLength() > 0;		
	}
	
	/**
	 * Set the prefix for the logic tags contained in the service interface definition.
	 * 
	 * @since 5.6.0
	 */
	private void setPrefix()
	{
		//sets the prefix to the defined attribute, in order not to execute this verificatin everytime it needs the value of the 
		//logic tag prefix
		this.tagPrefix = serviceInterfaceDefinitionNode.getOwnerDocument().lookupPrefix(LOGIC_NAMESPACE_URI);
	}

}